<?php

// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2014 The Herosphp Authors. All rights reserved.
// * Use of this source code is governed by a MIT-style license
// * that can be found in the LICENSE file.
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

namespace herosphp\plugin\storage\core;

use Exception;
use herosphp\plugin\storage\handler\FileHandler;
use herosphp\utils\FileUtil;

/**
 * 文件上传工具, 支持多文件上传,支持 base64 编码文件上传
 * ---------------------------------------------------------------------
 * @author RockYang<yangjian102621@gmail.com>
 */
class Uploader
{
    // Allowed file extension
    protected string $_allowExt = '*';

    // Refuse file extension
    protected string $_rejectExt = '';

    // Allowed max file size, default value is  5MiB,
    // if no limits, set it to 0, default: 5MiB
    protected int $_maxFileSize = 5242880;

    /**
     * File info of the uploaded file
     * @see UploadFileInfo
     */
    protected array $_fileInfos = [];

    // Upload error
    protected ?Error $_error = null;

    // File save handler
    protected ?FileHandler $_handler = null;

    // Constructor
    public function __construct(int $maxFileSize, string $allowExt, string $rejectExt, FileHandler $handler)
    {
        $this->_maxFileSize = $maxFileSize;
        $this->_allowExt = $allowExt;
        $this->_rejectExt = $rejectExt;
        $this->_handler = $handler;

        ini_set('upload_max_filesize', ceil($this->_maxFileSize / 1048576) . 'M');
    }

    // Upload file
    public function upload(array $files = null): FileInfo|array|bool
    {
        if (empty($files)) {
            $this->_error = Error::get(UploadError::NO_FILE_UPLOAD);
            return false;
        }

        if ($this->_isMultiple($files)) {
            foreach ($files as $file) {
                $fileInfo = $this->_doUpload($file);
                if ($fileInfo !== false) {
                    $this->_fileInfos[] = $fileInfo;
                }
            }
        } else {
            return $this->_doUpload($files);
        }

        if (empty($this->_fileInfos)) {
            return false;
        }

        if (count($this->_fileInfos) !== count($files)) {
            $this->_error = Error::get(UploadError::PART_UPLOADED);
        }

        return $this->_fileInfos;
    }

    // get upload error
    public function getError(): ?Error
    {
        return $this->_error;
    }

    // upload base64 image data
    public function uploadBase64(string $data): FileInfo|bool
    {
        if (strlen($data) < 10) {
            $this->_error = Error::get(UploadError::NO_FILE_UPLOAD);
            return false;
        }

        if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $data, $match)) {
            $data = str_replace($match[1], '', $data);
        }

        $filename = static::_genFilename('png');
        $fileInfo = new FileInfo($filename, strlen($data), 'png', 'image/png');
        $fileInfo->name = $filename;

        [$ret, $err] = $this->_handler->saveBase64($data, $fileInfo->name);
        if ($err !== null) {
            $this->_error = $err;
            return false;
        }

        if (str_starts_with($fileInfo->mimeType, 'image')) {
            $fileInfo->isImage = true;
        }

        if (is_array($ret)) {
            $fileInfo->url = $ret['url'];
            $fileInfo->path = $ret['path'];
        } else {
            $fileInfo->url = $ret;
        }

        return $fileInfo;
    }

    public function size(int $size): void
    {
        if ($size >= 0) {
            $this->_maxFileSize = $size;
        }
    }

    public function allow(string $ext): void
    {
        if ($ext) {
            $this->_allowExt = $ext;
        }
    }

    public function deny(string $ext): void
    {
        if ($ext) {
            $this->_rejectExt = $ext;
        }
    }

    // do upload single file
    protected function _doUpload(array $file): FileInfo|bool
    {
        $ext = FileUtil::getFileExtension($file['name']);
        $check = $this->_checkFileExtension($ext) && $this->_checkFileSize($file['tmp_name']);
        if ($check === false) {
            return false;
        }

        $fileInfo = new FileInfo($file['name'], $file['size'], $ext, $file['type']);
        $fileInfo->name = static::_genFilename($ext);
        if (str_starts_with($fileInfo->mimeType, 'image')) {
            $fileInfo->isImage = true;
        }

        [$ret, $err] = $this->_handler->save($file['tmp_name'], $fileInfo->name);
        if ($err !== null) {
            $this->_error = $err;
            return false;
        }

        if (is_array($ret)) {
            $fileInfo->url = $ret['url'];
            $fileInfo->path = $ret['path'];
        } else {
            $fileInfo->url = $ret;
        }

        return $fileInfo;
    }

    // generate a filename
    protected static function _genFilename(string $ext): string
    {
        try {
            return bin2hex(pack('d', microtime(true)) . random_bytes(8)) . ".$ext";
        } catch (Exception) {
            return  bin2hex(pack('d', microtime(true)) . ".$ext");
        }
    }

    // check if is multiple upload
    protected function _isMultiple(array $files): bool
    {
        $value = array_values($files);
        $diff = array_diff_key($files, $value);
        return empty($diff);
    }

    // check file extension
    protected function _checkFileExtension(string $ext): bool
    {
        if ($this->_allowExt === '*' && $this->_rejectExt === '') {
            return true;
        }

        $ext = strtolower($ext);
        $allowedExt = explode('|', $this->_allowExt);
        $refusedExt = explode('|', $this->_rejectExt);
        if (!in_array($ext, $allowedExt) || in_array($ext, $refusedExt)) {
            $this->_error = Error::get(UploadError::EXT_NOT_ALLOW);
            return false;
        }
        return true;
    }

    // check file size
    protected function _checkFileSize($path): bool
    {
        // no limit
        if ($this->_maxFileSize === 0) {
            return true;
        }

        if (filesize($path) > $this->_maxFileSize) {
            $this->_error = Error::get(UploadError::FILESIZE_OVER_LIMIT);
            return false;
        }

        return true;
    }
}
